package com.woniu.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.api.R;
import com.woniu.entity.SecKillBook;
import com.woniu.entity.SecKillOrder;
import com.woniu.entity.dto.SecBookDTO;
import com.woniu.entity.vo.SecActiveVo;
import com.woniu.mapper.SecKillMapper;
import com.woniu.product.DelayProduct;
import com.woniu.product.SecActiveProduct;
import com.woniu.product.TopicProduct;
import com.woniu.service.SecKillBookService;
import com.woniu.util.JWTUtil;
import com.woniu.util.ResData;
import com.woniu.util.ResEnum;
import com.woniu.util.SnowflakeIdGenerator;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.TimeUnit;

@Service
public class SecKillBookServiceImpl implements SecKillBookService {
    @Autowired
    private SecKillMapper secKillMapper;
    @Autowired
    private RedissonClient redissonClient;//config包下RedissConfig里的bean
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    @Autowired
    private DelayProduct delayProduct;
    @Autowired
    private TopicProduct topicProduct;
    @Autowired
    private SecActiveProduct secActiveProduct;

    /**
     * 添加书籍到秒杀表
     *
     * 秒杀书籍，只会提前半小时放入 redis
     * 在秒杀结束后，把redis中数据删除
     *
     * @param secKillBook
     * @return
     */
    @Override
    @Transactional
    public ResData<?> addKillBook(SecKillBook secKillBook) {
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            //获取秒杀的开始时间
            Date startTime = sdf.parse(secKillBook.getStartTime());
            //计算 开始时间 减去 当前时间
            Long subTime = startTime.getTime()-new Date().getTime();
//            if(subTime > 30*60*1000){
            if(true){
                //计算出 ttl 时间，发送一个延迟队列
                long ttl = subTime-30*60*1000;
//                delayProduct.send(JSON.toJSONString(secKillBook),"addSecKill",(int)ttl);
                delayProduct.send(JSON.toJSONString(secKillBook),"addSecKill",5000);
            }else {
                //添加到秒杀表
                secKillMapper.addKillBook(secKillBook);
                //添加到 redis  redis的五种基本类型（String，list，set，zset，hash）
                //redis String 类型的key 有一个最小的固定大小（512字节）
    //        redisTemplate.opsForValue().set("seckill-"+secKillBook.getBookId(),secKillBook);
                redisTemplate.opsForHash().put("seckillBook", secKillBook.getBookId() + "", secKillBook);
                //发送一个删除的延迟队列
                //获取秒杀的结束时间
                Date endTime = sdf.parse(secKillBook.getEndTime());
                //计算 结束时间 减去 当前时间
                long nowTime = new Date().getTime();
                if(nowTime<endTime.getTime()){
                    long ttl = endTime.getTime() - nowTime;
                    delayProduct.send(JSON.toJSONString(secKillBook),"delSecKill",(int) ttl);
                }else {
                    //从秒杀表删除，但是暂时只删除 redis
                    //secKillMapper.addKillBook(secKillBook);
                    redisTemplate.opsForHash().delete("seckillBook", secKillBook.getBookId() + "");
                }
            }
            return new ResData<>().ok();
        } catch (ParseException e) {
            e.printStackTrace();
            return new ResData<>().fail(ResEnum.FAIL);
        }

    }

    /**
     * 查询 redis中所有秒杀的书籍
     *
     * @return
     */
    @Override
    public ResData<?> queryAll() {
        List<SecKillBook> secKillBooks = new ArrayList<>();
        Map<Object, Object> seckillBook = redisTemplate.opsForHash().entries("seckillBook");
        if (seckillBook == null) {
            return new ResData<>().fail(ResEnum.FAIL);
        }
        seckillBook.forEach((k, v) -> {
            secKillBooks.add((SecKillBook) v);
        });
        return new ResData<>().ok(secKillBooks);
    }

    /**
     * 根据书籍ID 查询 redis 中秒杀书籍的详细信息
     *
     * @param bookId
     * @return
     */
    @Override
    public ResData<?> queryDetail(Integer bookId) {
        SecKillBook seckillBook = (SecKillBook) redisTemplate.opsForHash().get("seckillBook", bookId + "");
        if (seckillBook == null) {
            return new ResData<>().fail(ResEnum.FAIL);
        }
        return new ResData<>().ok(seckillBook);
    }

    /**
     * 用户的抢购接口
     * 1：判断库存够不够
     * 如果不够，提示秒杀结束
     * 够：库存减一
     * <p>
     * 分布式锁
     * 为什么要用分布锁：
     * 在系统集群部署，分布式部署（分布式：分开布署的方式），
     * jdk 锁不住对共享资源的访问，
     * 所以才需要分布式锁
     * 怎么写一个分布式锁：
     * 原理：是利用 redis 一个 setnx 指令
     * setnx（在把数据放入 redis 时会判断该key是否存在，存在返回0，不存在返回1）
     * <p>
     * 利用 setnx自己实现一个分布式锁：
     * 1、在删除锁的时候，不是原子性操作，会有bug；
     * 2、锁的时间不好指定
     * 基于以上两个问题，可以使用 redssion 框架来实现分布式锁
     * <p>
     * 2：生成订单（异步生成）
     *
     * @param bookId
     * @param jwt
     * @return
     */
    @Override
    public ResData<?> startKill(Integer bookId, String jwt) {
        //1：获取 redission 锁对象
        RLock lock = redissonClient.getLock("lock");

        try {
            SecKillBook seckillBook = (SecKillBook) redisTemplate.opsForHash().get("seckillBook", bookId + "");
            if (seckillBook != null && seckillBook.getKillNum() > 0) {
                //2：尝试获取锁
                // tryLock()：
                //      锁的时间默认是 30s
                //      （watch dog）还会开一个子线程，作用：检查主线程是否走完 30s/3（30秒的三分之一），如果主线程没有走完，再续 30秒
//                boolean b = lock.tryLock();
                // 参数1：锁的时间；参数2：尝试获取锁的等待时间；参数3:时间单位（前面两个都影响）
                boolean b = lock.tryLock(20,2,TimeUnit.SECONDS);
                if(b){
                    //判断库存够不够
                    seckillBook.setKillNum(seckillBook.getKillNum() - 1);
                    System.out.println("库存：" + seckillBook.getKillNum());
                    redisTemplate.opsForHash().put("seckillBook", bookId + "", seckillBook);

                    //生成订单（异步生成）
                    SecKillOrder secKillOrder = new SecKillOrder();
                    secKillOrder.setKillGoodId(seckillBook.getBookId());
                    secKillOrder.setKillNum(1);
                    secKillOrder.setPrice(seckillBook.getDiscountPrice());
                    Map userinfo = JWTUtil.getJWTUserInfo(jwt);
                    Long userId = (Long) userinfo.get("userId");
                    secKillOrder.setUserId(userId.intValue());
                    SnowflakeIdGenerator snowflakeIdGenerator = new SnowflakeIdGenerator();
                    long orderNum = snowflakeIdGenerator.nextId();
                    secKillOrder.setKillOrderNum(String.valueOf(orderNum));

                    topicProduct.send(JSON.toJSONString(secKillOrder),"seckill.order");

                    return new ResData<>().ok();
                }else {
                    //没有获取到锁
                    return new ResData<>().fail(ResEnum.FAIL);
                }

            } else {
                return new ResData<>().fail(ResEnum.FAIL);
            }
        } catch (Exception e) {
            return new ResData<>().fail(ResEnum.FAIL);
        } finally {
            //释放锁
            if(lock.isLocked() && lock.isHeldByCurrentThread()){
                lock.unlock();  //释放锁
            }
        }

    }

    @Override
    public ResData<?> addActive(SecActiveVo secActive,String jwt) {
        try {
            Map userInfo = JWTUtil.getJWTUserInfo(jwt);
            Long userId = (Long) userInfo.get("userId");
            secActive.setUserId(userId.intValue());
            secActiveProduct.send(String.valueOf(secActive),"addKill",5000);
            return new ResData<>().ok();
        } catch (ParseException e) {
            return new ResData<>().fail(ResEnum.FAIL);
        }
    }

    public synchronized ResData<?> startKillA(Integer bookId, String jwt) {
        String uudId = UUID.randomUUID().toString();
        try {
            SecKillBook seckillBook = (SecKillBook) redisTemplate.opsForHash().get("seckillBook", bookId + "");
            if (seckillBook != null && seckillBook.getKillNum() > 0) {
                //加分布式锁 setIfAbsent() 封装了 setnx指令
                // 随便存一key进redis，如果返回1，说明当前线程是第一个进来的，可以操作，
                //最后将加的值删掉就算解锁（需要考虑只能删除自己的锁，以唯一的value值实现
                Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent("lock", uudId, 30, TimeUnit.SECONDS);
                if (aBoolean) {
                    //判断库存够不够
                    seckillBook.setKillNum(seckillBook.getKillNum() - 1);
                    System.out.println("库存：" + seckillBook.getKillNum());
                    redisTemplate.opsForHash().put("seckillBook", bookId + "", seckillBook);

                    //生成订单（异步生成）
                    return new ResData<>().ok();
                } else {
                    return new ResData<>().fail(ResEnum.FAIL);
                }

            } else {
                return new ResData<>().fail(ResEnum.FAIL);
            }
        } finally {
            if (uudId.equals(redisTemplate.opsForValue().get("lock"))) {
                // 释放锁 redis的脚本语言（lua脚本）
                // 仅靠java是无法实现分布式锁的，只能使用框架，或者自学 lua脚本
                redisTemplate.delete("lock");
            }

        }

    }


}
